<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
 <head>
  <meta http-equiv="content-type" content="text/html; charset=UTF-8">
  <title>SQL 注入</title>
<link media="all" rel="stylesheet" type="text/css" href="styles/03e73060321a0a848018724a6c83de7f-theme-base.css" />
<link media="all" rel="stylesheet" type="text/css" href="styles/03e73060321a0a848018724a6c83de7f-theme-medium.css" />

 </head>
 <body class="docs"><div class="navbar navbar-fixed-top">
  <div class="navbar-inner clearfix">
    <ul class="nav" style="width: 100%">
      <li style="float: left;"><a href="security.database.storage.html">« 加密存储模型</a></li>
      <li style="float: right;"><a href="security.errors.html">错误报告 »</a></li>
    </ul>
  </div>
</div>
<div id="breadcrumbs" class="clearfix">
  <ul class="breadcrumbs-container">
    <li><a href="index.html">PHP Manual</a></li>
    <li><a href="security.database.html">数据库安全</a></li>
    <li>SQL 注入</li>
  </ul>
</div>
<div id="layout">
  <div id="layout-content"><div id="security.database.sql-injection" class="sect1">
    <h2 class="title">SQL 注入</h2>
    <p class="simpara">
     很多 web 开发者没有注意到 SQL 查询是可以被篡改的，因而把 SQL
     查询当作可信任的命令。殊不知道，SQL
     查询可以绕开访问控制，从而绕过身份验证和权限检查。更有甚者，有可能通过 SQL
     查询去运行主机操作系统级的命令。
    </p>
    <p class="simpara">
     直接 SQL 命令注入就是攻击者常用的一种创建或修改已有 SQL
     语句的技术，从而达到取得隐藏数据，或覆盖关键的值，甚至执行数据库主机操作系统命令的目的。这是通过应用程序取得用户输入并与静态参数组合成
     SQL 查询来实现的。下面将会给出一些真实的例子。
    </p>
    <p class="para">
     由于在缺乏对输入的数据进行验证，并且使用了超级用户或其它有权创建新用户的数据库帐号来连接，攻击者可以在数据库中新建一个超级用户。
     <div class="example" id="example-436">
      <p><strong>示例 #1 
       一段实现数据分页显示的代码…… 也可以被用作创建一个超级用户（PostgreSQL 数据库）。
      </strong></p>
      <div class="example-contents">
<div class="phpcode"><code><span style="color: #000000">
<span style="color: #0000BB">&lt;?php<br /><br />$offset&nbsp;</span><span style="color: #007700">=&nbsp;</span><span style="color: #0000BB">$argv</span><span style="color: #007700">[</span><span style="color: #0000BB">0</span><span style="color: #007700">];&nbsp;</span><span style="color: #FF8000">//&nbsp;注意，没有输入验证！<br /></span><span style="color: #0000BB">$query&nbsp;&nbsp;</span><span style="color: #007700">=&nbsp;</span><span style="color: #DD0000">"SELECT&nbsp;id,&nbsp;name&nbsp;FROM&nbsp;products&nbsp;ORDER&nbsp;BY&nbsp;name&nbsp;LIMIT&nbsp;20&nbsp;OFFSET&nbsp;</span><span style="color: #0000BB">$offset</span><span style="color: #DD0000">;"</span><span style="color: #007700">;<br /></span><span style="color: #0000BB">$result&nbsp;</span><span style="color: #007700">=&nbsp;</span><span style="color: #0000BB">pg_query</span><span style="color: #007700">(</span><span style="color: #0000BB">$conn</span><span style="color: #007700">,&nbsp;</span><span style="color: #0000BB">$query</span><span style="color: #007700">);<br /><br /></span><span style="color: #0000BB">?&gt;</span>
</span>
</code></div>
      </div>

     </div>
      一般的用户会点击 <var class="varname">$offset</var>
      已被斌值的“上一页”、“下一页”的链接。原本代码只会认为 <var class="varname">$offset</var>
      是一个数值。然而，如果有人尝试把以下语句先经过 <span class="function"><a href="function.urlencode.html" class="function">urlencode()</a></span>
      处理，然后加入URL中的话：
      <div class="informalexample">
       <div class="example-contents">
<div class="sqlcode"><pre class="sqlcode">0;
insert into pg_shadow(usename,usesysid,usesuper,usecatupd,passwd)
    select &#039;crack&#039;, usesysid, &#039;t&#039;,&#039;t&#039;,&#039;crack&#039;
    from pg_shadow where usename=&#039;postgres&#039;;
--</pre>
</div>
       </div>

      </div>
      那么他就可以创建一个超级用户了。注意那个 <code class="literal">0;</code>
      只不过是为了提供一个正确的偏移量以便补充完整原来的查询，使它不要出错而已。
    </p>
    <blockquote class="note"><p><strong class="note">注意</strong>: 
     <p class="para">
      <code class="literal">--</code> 是 SQL 的注释标记，一般可以使用来它告诉 SQL 解释器忽略后面的语句。
     </p>
    </p></blockquote>
    <p class="para">
     对显示搜索结果的页面下手是一个能得到密码的可行办法。攻击者所要做的只不过是找出哪些提交上去的变量是用于
     SQL 语句并且处理不当的。而这类的变量通常都被用于 <code class="literal">SELECT</code>
     查询中的条件语句，如 <code class="literal">WHERE, ORDER BY, LIMIT</code> 和
     <code class="literal">OFFSET</code>。如果数据库支持 <code class="literal">UNION</code>
     构造的话，攻击者还可能会把一个完整的 SQL
     查询附加到原来的语句上以便从任意数据表中得到密码。因此，对密码字段加密是很重要的。
     <div class="example" id="example-437">
      <p><strong>示例 #2 
       显示文章……以及一些密码（任何数据库系统）
      </strong></p>
      <div class="example-contents">
<div class="phpcode"><code><span style="color: #000000">
<span style="color: #0000BB">&lt;?php<br /><br />$query&nbsp;&nbsp;</span><span style="color: #007700">=&nbsp;</span><span style="color: #DD0000">"SELECT&nbsp;id,&nbsp;name,&nbsp;inserted,&nbsp;size&nbsp;FROM&nbsp;products<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;WHERE&nbsp;size&nbsp;=&nbsp;'</span><span style="color: #0000BB">$size</span><span style="color: #DD0000">'"</span><span style="color: #007700">;<br /></span><span style="color: #0000BB">$result&nbsp;</span><span style="color: #007700">=&nbsp;</span><span style="color: #0000BB">odbc_exec</span><span style="color: #007700">(</span><span style="color: #0000BB">$conn</span><span style="color: #007700">,&nbsp;</span><span style="color: #0000BB">$query</span><span style="color: #007700">);<br /><br /></span><span style="color: #0000BB">?&gt;</span>
</span>
</code></div>
      </div>

     </div>
     可以在原来的查询的基础上添加另一个
     <code class="literal">SELECT</code> 查询来获得密码：
     <div class="informalexample">
      <div class="example-contents">
<div class="sqlcode"><pre class="sqlcode">&#039;
union select &#039;1&#039;, concat(uname||&#039;-&#039;||passwd) as name, &#039;1971-01-01&#039;, &#039;0&#039; from usertable;
--</pre>
</div>
      </div>

     </div>
     假如上述语句（使用 <code class="literal">&#039;</code> 和
     <code class="literal">--</code>）被加入到 <var class="varname">$query</var>
     中的任意一个变量的话，那么就麻烦了。
    </p>
    <p class="para">
     SQL 中的 UPDATE
     也会受到攻击。这种查询也可能像上面的例子那样被插入或附加上另一个完整的请求。但是攻击者更愿意对
     <code class="literal">SET</code>
     子句下手，这样他们就可以更改数据表中的一些数据。这种情况下必须要知道数据库的结构才能修改查询成功进行。可以通过表单上的变量名对字段进行猜测，或者进行暴力破解。对于存放用户名和密码的字段，命名的方法并不多。
     <div class="example" id="example-438">
     <p><strong>示例 #3 
      从重设密码……到获得更多权限（任何数据库系统）
     </strong></p>
      <div class="example-contents">
<div class="phpcode"><code><span style="color: #000000">
<span style="color: #0000BB">&lt;?php<br />$query&nbsp;</span><span style="color: #007700">=&nbsp;</span><span style="color: #DD0000">"UPDATE&nbsp;usertable&nbsp;SET&nbsp;pwd='</span><span style="color: #0000BB">$pwd</span><span style="color: #DD0000">'&nbsp;WHERE&nbsp;uid='</span><span style="color: #0000BB">$uid</span><span style="color: #DD0000">';"</span><span style="color: #007700">;<br /></span><span style="color: #0000BB">?&gt;</span>
</span>
</code></div>
      </div>

     </div>
     但是恶意的用户会把 <code class="literal">&#039; or uid like&#039;%admin%&#039;; --</code>
     作为变量的值提交给 <var class="varname">$uid</var> 来改变 admin 的密码，或者把
     <var class="varname">$pwd</var> 的值提交为 <code class="literal">&quot;hehehe&#039;, admin=&#039;yes&#039;,
     trusted=100 &quot;</code>（后面有个空格）去获得更多的权限。这样做的话，查询语句实际上就变成了：
     <div class="informalexample">
      <div class="example-contents">
<div class="phpcode"><code><span style="color: #000000">
<span style="color: #0000BB">&lt;?php<br /><br /></span><span style="color: #FF8000">//&nbsp;$uid:&nbsp;'&nbsp;or&nbsp;uid&nbsp;like&nbsp;'%admin%<br /></span><span style="color: #0000BB">$query&nbsp;</span><span style="color: #007700">=&nbsp;</span><span style="color: #DD0000">"UPDATE&nbsp;usertable&nbsp;SET&nbsp;pwd='...'&nbsp;WHERE&nbsp;uid=''&nbsp;or&nbsp;uid&nbsp;like&nbsp;'%admin%';"</span><span style="color: #007700">;<br /><br /></span><span style="color: #FF8000">//&nbsp;$pwd:&nbsp;hehehe',&nbsp;trusted=100,&nbsp;admin='yes<br /></span><span style="color: #0000BB">$query&nbsp;</span><span style="color: #007700">=&nbsp;</span><span style="color: #DD0000">"UPDATE&nbsp;usertable&nbsp;SET&nbsp;pwd='hehehe',&nbsp;trusted=100,&nbsp;admin='yes'&nbsp;WHERE<br />...;"</span><span style="color: #007700">;<br /><br /></span><span style="color: #0000BB">?&gt;</span>
</span>
</code></div>
      </div>

     </div>
    </p>
    <p class="para">
     下面这个可怕的例子将会演示如何在某些数据库上执行系统命令。
     <div class="example" id="example-439">
     <p><strong>示例 #4 攻击数据库所在主机的操作系统（MSSQL Server 数据库）</strong></p>
      <div class="example-contents">
<div class="phpcode"><code><span style="color: #000000">
<span style="color: #0000BB">&lt;?php<br /><br />$query&nbsp;&nbsp;</span><span style="color: #007700">=&nbsp;</span><span style="color: #DD0000">"SELECT&nbsp;*&nbsp;FROM&nbsp;products&nbsp;WHERE&nbsp;id&nbsp;LIKE&nbsp;'%</span><span style="color: #0000BB">$prod</span><span style="color: #DD0000">%'"</span><span style="color: #007700">;<br /></span><span style="color: #0000BB">$result&nbsp;</span><span style="color: #007700">=&nbsp;</span><span style="color: #0000BB">mssql_query</span><span style="color: #007700">(</span><span style="color: #0000BB">$query</span><span style="color: #007700">);<br /><br /></span><span style="color: #0000BB">?&gt;</span>
</span>
</code></div>
      </div>

     </div>
     如果攻击提交
     <code class="literal">a%&#039; exec master..xp_cmdshell &#039;net user test testpass /ADD&#039; --</code>
     作为变量 <var class="varname">$prod</var>的值，那么
     <var class="varname">$query</var> 将会变成
     <div class="informalexample">
      <div class="example-contents">
<div class="phpcode"><code><span style="color: #000000">
<span style="color: #0000BB">&lt;?php<br /><br />$query&nbsp;&nbsp;</span><span style="color: #007700">=&nbsp;</span><span style="color: #DD0000">"SELECT&nbsp;*&nbsp;FROM&nbsp;products<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;WHERE&nbsp;id&nbsp;LIKE&nbsp;'%a%'<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;exec&nbsp;master..xp_cmdshell&nbsp;'net&nbsp;user&nbsp;test&nbsp;testpass&nbsp;/ADD'&nbsp;--%'"</span><span style="color: #007700">;<br /></span><span style="color: #0000BB">$result&nbsp;</span><span style="color: #007700">=&nbsp;</span><span style="color: #0000BB">mssql_query</span><span style="color: #007700">(</span><span style="color: #0000BB">$query</span><span style="color: #007700">);<br /><br /></span><span style="color: #0000BB">?&gt;</span>
</span>
</code></div>
      </div>

     </div>
     MSSQL 服务器会执行这条 SQL
     语句，包括它后面那个用于向系统添加用户的命令。如果这个程序是以
     <code class="literal">sa</code> 运行而 MSSQLSERVER
     服务又有足够的权限的话，攻击者就可以获得一个系统帐号来访问主机了。
    </p>
    <blockquote class="note"><p><strong class="note">注意</strong>: 
     <p class="para">
      虽然以上的例子是针对某一特定的数据库系统的，但是这并不代表不能对其它数据库系统实施类似的攻击。使用不同的方法，各种数据库都有可能遭殃。
     </p>
    </p></blockquote>
    <p class="para">
     <div class="mediaobject">
      
      <div class="imageobject">
       <img src="images/fa7c5b5f326e3c4a6cc9db19e7edbaf0-xkcd-bobby-tables.png" alt="关于 SQL 注入问题的工作实例" width="666" height="205" />
      </div>
     </div>
     Image courtesy of <a href="http://xkcd.com/327" class="link external">&raquo;&nbsp;xkcd</a>
    </p>

    <div class="sect2" id="security.database.avoiding">
     <h3 class="title">预防措施</h3>
     <p class="simpara">
      也许有人会自我安慰，说攻击者要知道数据库结构的信息才能实施上面的攻击。没错，确实如此。但没人能保证攻击者一定得不到这些信息，一但他们得到了，数据库有泄露的危险。如果你在用开放源代码的软件包来访问数据库，比如论坛程序，攻击者就很容得到到相关的代码。如果这些代码设计不良的话，风险就更大了。
     </p>
     <p class="simpara">
      这些攻击总是建立在发掘安全意识不强的代码上的。所以，永远不要信任外界输入的数据，特别是来自于客户端的，包括选择框、表单隐藏域和
      cookie。就如上面的第一个例子那样，就算是正常的查询也有可能造成灾难。
     </p>

     <ul class="itemizedlist">
      <li class="listitem">
       <span class="simpara">
        永远不要使用超级用户或所有者帐号去连接数据库。要用权限被严格限制的帐号。
       </span>
      </li>
      <li class="listitem">
       <span class="simpara">
        检查输入的数据是否具有所期望的数据格式。PHP
        有很多可以用于检查输入的函数，从简单的
        <a href="ref.var.html" class="link">变量函数</a> 和
        <a href="ref.ctype.html" class="link">字符类型函数</a>（比如
        <span class="function"><a href="function.is-numeric.html" class="function">is_numeric()</a></span>，<span class="function"><a href="function.ctype-digit.html" class="function">ctype_digit()</a></span>）到复杂的
        <a href="ref.pcre.html" class="link">Perl 兼容正则表达式函数</a>都可以完成这个工作。
       </span>
      </li>
      <li class="listitem">
       <p class="para">
        如果程序等待输入一个数字，可以考虑使用 <span class="function"><a href="function.is-numeric.html" class="function">is_numeric()</a></span>
        来检查，或者直接使用 <span class="function"><a href="function.settype.html" class="function">settype()</a></span> 来转换它的类型，也可以用
        <span class="function"><a href="function.sprintf.html" class="function">sprintf()</a></span> 把它格式化为数字。
        <div class="example" id="example-440">
         <p><strong>示例 #5 一个实现分页更安全的方法</strong></p>
         <div class="example-contents">
<div class="phpcode"><code><span style="color: #000000">
<span style="color: #0000BB">&lt;?php<br /><br />settype</span><span style="color: #007700">(</span><span style="color: #0000BB">$offset</span><span style="color: #007700">,&nbsp;</span><span style="color: #DD0000">'integer'</span><span style="color: #007700">);<br /></span><span style="color: #0000BB">$query&nbsp;</span><span style="color: #007700">=&nbsp;</span><span style="color: #DD0000">"SELECT&nbsp;id,&nbsp;name&nbsp;FROM&nbsp;products&nbsp;ORDER&nbsp;BY&nbsp;name&nbsp;LIMIT&nbsp;20&nbsp;OFFSET&nbsp;</span><span style="color: #0000BB">$offset</span><span style="color: #DD0000">;"</span><span style="color: #007700">;<br /><br /></span><span style="color: #FF8000">//&nbsp;请注意格式字符串中的&nbsp;%d，如果用&nbsp;%s&nbsp;就毫无意义了<br /></span><span style="color: #0000BB">$query&nbsp;</span><span style="color: #007700">=&nbsp;</span><span style="color: #0000BB">sprintf</span><span style="color: #007700">(</span><span style="color: #DD0000">"SELECT&nbsp;id,&nbsp;name&nbsp;FROM&nbsp;products&nbsp;ORDER&nbsp;BY&nbsp;name&nbsp;LIMIT&nbsp;20&nbsp;OFFSET&nbsp;%d;"</span><span style="color: #007700">,<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="color: #0000BB">$offset</span><span style="color: #007700">);<br /><br /></span><span style="color: #0000BB">?&gt;</span>
</span>
</code></div>
         </div>

        </div>
       </p>
      </li>
      <li class="listitem">
       <span class="simpara">
        使用数据库特定的敏感字符转义函数（比如 <span class="function"><a href="function.mysql-escape-string.html" class="function">mysql_escape_string()</a></span> 和
        <span class="function"><strong>sql_escape_string()</strong></span>）把用户提交上来的非数字数据进行转义。如果数据库没有专门的敏感字符转义功能的话
        <span class="function"><a href="function.addslashes.html" class="function">addslashes()</a></span> 和 <span class="function"><a href="function.str-replace.html" class="function">str_replace()</a></span>
        可以代替完成这个工作。看看<a href="security.database.storage.html" class="link">第一个例子</a>，此例显示仅在查询的静态部分加上引号是不够的，查询很容易被攻破。
       </span>
      </li>
      <li class="listitem">
       <span class="simpara">
        要不择手段避免显示出任何有关数据库的信心，尤其是数据库结构。参见<a href="security.errors.html" class="link">错误报告</a>和<a href="ref.errorfunc.html" class="link">错误处理函数</a>。
       </span>
      </li>
      <li class="listitem">
       <span class="simpara">
        也可以选择使用数据库的存储过程和预定义指针等特性来抽象数库访问，使用户不能直接访问数据表和视图。但这个办法又有别的影响。
       </span>
      </li>
     </ul>

     <p class="simpara">
      除此之外，在允许的情况下，使用代码或数据库系统保存查询日志也是一个好办法。显然，日志并不能防止任何攻击，但利用它可以跟踪到哪个程序曾经被尝试攻击过。日志本身没用，要查阅其中包含的信息才行。毕竟，更多的信息总比没有要好。
     </p>
    </div>
   </div></div></div></body></html>